// (c) Copyright 2013-2014 Stephen Gold <sgold@sonic.net>
// Distributed under the terms of the GNU General Public License

/*
 This file is part of the Bats Game.

 The Bats Game is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by the
 Free Software Foundation, either version 3 of the License, or (at your
 option) any later version.

 The Bats Game is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 for more details.

 You should have received a copy of the GNU General Public License
 along with the Bats Game.  If not, see <http://www.gnu.org/licenses/>.
 */
package bats.scene;

import bats.Assets;
import bats.SpatialProperties;
import bats.control.PickedControl;
import bats.indicator.Cursor3DIndicator;
import com.jme3.bullet.PhysicsSpace;
import com.jme3.bullet.collision.shapes.CollisionShape;
import com.jme3.bullet.collision.shapes.CompoundCollisionShape;
import com.jme3.bullet.collision.shapes.CylinderCollisionShape;
import com.jme3.bullet.collision.shapes.GImpactCollisionShape;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.material.Material;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import jme3utilities.MySpatial;
import jme3utilities.MyString;
import jme3utilities.NameGenerator;
import jme3utilities.math.MyMath;

/**
 * A world component to manage props.
 *
 * @author Stephen Gold <sgold@sonic.net>
 */
public class Props {
    // *************************************************************************
    // constants

    /**
     * coefficient of friction (&ge;0)
     */
    final private static float friction = 1f;
    /**
     * message logger for this class
     */
    final private static Logger logger =
            Logger.getLogger(Props.class.getName());
    /**
     * pickType for props
     */
    final private static String pickType = "prop";
    // *************************************************************************
    // fields
    /**
     * scene where these props reside: set by constructor
     */
    final private SceneNode scene;
    // *************************************************************************
    // constructors

    /**
     * Instantiate for a specified scene.
     *
     * @param scene where these props reside (not null)
     */
    public Props(SceneNode scene) {
        assert scene != null;
        this.scene = scene;
    }
    // *************************************************************************
    // new methods exposed

    /**
     * Create a prop and add it to the scene. Assumes that the application is in
     * world-building mode.
     *
     * @param propType which kind of prop to add (not null)
     * @param relativeSize the prop's size relative to standard (&gt;0)
     * @param centerLocation world coordinates for the prop's center
     * (unaffected, not null)
     * @param orientation initial orientation of the prop (unaffected, unit
     * vector)
     */
    Node add(String propType, float relativeSize, Vector3f centerLocation,
            Quaternion orientation) {
        assert propType != null;
        assert relativeSize > 0f : relativeSize;
        assert orientation != null;
        /*
         * Create the prop and add it to the scene.
         */
        Node node = create(propType, relativeSize);
        scene.getWorld().getPickables().add(node);
        /*
         * Position the prop.
         */
        MySpatial.setWorldOrientation(node, orientation);
        MySpatial.setWorldLocation(node, centerLocation);

        return node;
    }

    /**
     * Duplicate a prop. Duplicating differs from cloning because the duplicate
     * gets a unique name and any descendants of the duplicate get renamed to
     * match. Besides, cloning doesn't work well on physics controls.
     *
     * @param originalProp which prop to duplicate (not null)
     * @return the new prop
     */
    public Node duplicate(Spatial originalProp) {
        String name = originalProp.getName();
        logger.log(Level.INFO, "duplicating prop {0}", MyString.quote(name));

        String propType = SpatialProperties.getPrefixType(originalProp);
        float relativeSize = originalProp.getWorldScale().x;
        Vector3f centerLocation = MySpatial.getWorldLocation(originalProp);
        Quaternion orientation = MySpatial.getWorldOrientation(originalProp);

        Node result = add(propType, relativeSize, centerLocation, orientation);
        return result;
    }

    /**
     * Get the world coordinates for a prop's pick indicator.
     *
     * @param spatial which prop (not null)
     * @return a new vector
     */
    public static Vector3f getIndicatorLocation(Spatial spatial) {
        Vector3f local = new Vector3f(0f, 0f, 0f);
        Vector3f result = spatial.localToWorld(local, null);
        return result;
    }

    /**
     * Get the relative size of a prop.
     *
     * @param spatial which prop (not null)
     * @return the prop's size relative to standard (&gt;0)
     */
    public float getRelativeSize(Spatial spatial) {
        assert spatial != null;

        float result = spatial.getWorldScale().x;
        result *= scene.getScaleFactor();
        return result;
    }

    /**
     * Process a hotkey or button action which has just begun.
     *
     * @param words action string split into an array of words (not null)
     * @return true if the action was handled, false if it wasn't handled
     */
    public boolean processAction(String[] words) {
        String verb = words[0];
        if ("add".equals(verb)) {
            assert words.length > 1;
            String what = words[1];
            if ("prop".equals(what) && words.length > 2) {
                /*
                 * Add a prop at the insertion point.
                 */
                String name = words[2];
                addAsPick(name);
                return true;
            }
        }
        return false;
    }

    /**
     * Reload a prop and unpick it.
     *
     * @param originalProp which prop to reload (not null)
     */
    public void reload(Spatial originalProp) {
        String name = originalProp.getName();
        logger.log(Level.INFO, "reloading prop {0}", MyString.quote(name));
        Node reloaded = duplicate(originalProp);
        scene.deleteSpatial(originalProp);
        reloaded.setName(name);
    }
    // *************************************************************************
    // private methods

    /**
     * Create a prop, add it to the scene with its base at the insertion point,
     * and pick it. Assumes that the application is in world-building mode.
     *
     * @param propType which kind of prop to add (not null)
     */
    private void addAsPick(String propType) {
        assert propType != null;
        switch (propType) {
            case "barrel":
                addAsPick("barrel", 0.62f);
                return;
            case "hogshead":
                addAsPick("barrel", 0.78f);
                return;
            case "mantlet":
                addAsPick("mantlet", 1f);
                return;
            case "tun":
                addAsPick("barrel", 1.24f);
                return;
        }
        logger.log(Level.SEVERE, "Unknown prop type {0}!",
                MyString.quote(propType));
    }

    /**
     * Create a prop, add it to the scene with its base at the insertion point,
     * and pick it. Assumes that the application is in world-building mode.
     *
     * @param propType which kind of prop to add (not null)
     * @param relativeSize the prop's size relative to standard (&gt;0)
     */
    private void addAsPick(String propType, float relativeSize) {
        assert propType != null;
        assert relativeSize > 0f : relativeSize;
        /*
         * Create the prop.
         */
        Node node = create(propType, relativeSize);
        /*
         * The new prop is to be the ONLY picked spatial, so unpick the rest.
         */
        PickablesNode pickables = scene.getWorld().getPickables();
        pickables.clearPick();
        /*
         * Add and pick the prop.
         */
        pickables.add(node);
        SpatialProperties.setPicked(node, true);
        /*
         * Initialize the prop's position.
         */
        Cursor3DIndicator cursor3D = scene.getIndicators().getCursor3D();
        Vector3f baseLocation = cursor3D.getWorldLocation();
        float baseY = MySpatial.getMinY(node);
        Vector3f centerLocation = baseLocation.add(0f, -baseY, 0f);
        MySpatial.setWorldLocation(node, centerLocation);
    }

    /**
     * Compute the mass of a prop.
     *
     * @param propType which kind of prop (not null)
     * @param relativeSize the prop's size relative to standard (&gt;0)
     * @return mass (in kilograms, &gt;0)
     */
    private static float calculateMass(String propType, float relativeSize) {
        assert propType != null;
        assert relativeSize > 0f : relativeSize;

        float relativeVolume = MyMath.cube(relativeSize);
        float mass = standardMass(propType) * relativeVolume; // kilograms
        return mass;
    }

    /**
     * Create a prop without adding it to the scene.
     *
     * @param propType which kind of prop to add (not null)
     * @param relativeSize the prop's size relative to standard (&gt;0)
     * @return a new node to represent the prop, or null if the model is invalid
     */
    private Node create(String propType, float relativeSize) {
        assert propType != null;
        assert relativeSize > 0f : relativeSize;

        Node propNode = loadModel(propType);
        Node modelNode = (Node) propNode.getChild(0);
        List<Spatial> parts = modelNode.getChildren();
        /*
         * Texture each part of the prop and specify its hardness.
         */
        for (Spatial part : parts) {
            String partName = part.getName();
            if (!(part instanceof Geometry)) {
                logger.log(Level.SEVERE, "Prop part {0} is not a geometry.",
                        MyString.quote(partName));
                return null;
            }
            String materialName = material(partName);
            SpatialProperties.setHardness(part, materialName);
            Material material = Assets.loadBlockMaterial(materialName);
            part.setMaterial(material);
        }
        /*
         * Set the scale factor.
         */
        float scaleFactor = scene.getScaleFactor();
        float modelScale = relativeSize / scaleFactor;
        propNode.setLocalScale(modelScale);
        assert getRelativeSize(propNode) == relativeSize : relativeSize;
        /*
         * Set other properties.
         */
        propNode.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
        /*
         * Give the prop a physics control.
         */
        CollisionShape shape = createShape(propType, relativeSize, parts);
        float mass = calculateMass(propType, relativeSize);
        RigidBodyControl rigidBodyControl = new RigidBodyControl(shape, mass);
        propNode.addControl(rigidBodyControl);
        /*
         * Configure the physics control.
         */
        rigidBodyControl.setFriction(friction);
        scene.getWorld().getPhysics().addObject(rigidBodyControl);
        /*
         * Give the prop a PickedControl.
         */
        SpatialProperties.setPickType(propNode, pickType);
        PickedControl pickedControl = new PickedControl();
        propNode.addControl(pickedControl);

        return propNode;
    }

    /**
     * Generate a collision shape for a prop.
     *
     * @param propType the type of the prop (not null)
     * @param relativeSize the prop's size relative to standard (&gt;0)
     * @param parts (not null)
     * @return a new instance
     */
    private CollisionShape createShape(String propType,
            float relativeSize, List<Spatial> parts) {
        assert propType != null;
        assert relativeSize > 0f : relativeSize;
        assert parts != null;
        assert !parts.isEmpty();

        if ("barrel".equals(propType)) {
            /*
             * Generate a cylindrical shape aligned with the Y-axis.
             */
            float halfHeight = 0.5f * relativeSize; // meters
            float radius = 0.472f * relativeSize; // meters
            Vector3f halfExtents = new Vector3f(radius, halfHeight, radius);
            float scaleFactor = scene.getScaleFactor();
            halfExtents.divideLocal(scaleFactor);
            CollisionShape shape = new CylinderCollisionShape(halfExtents,
                    PhysicsSpace.AXIS_Y);
            return shape;
        }
        /*
         * Generate a compound shape composed of GImpact shapes,
         * one for each geometry.
         */
        CompoundCollisionShape shape = new CompoundCollisionShape();
        for (Spatial part : parts) {
            Geometry geometry = (Geometry) part;
            Mesh mesh = geometry.getMesh();
            CollisionShape partShape = new GImpactCollisionShape(mesh);
            Vector3f scale = part.getWorldScale();
            partShape.setScale(scale);
            shape.addChildShape(partShape, Vector3f.ZERO);
        }
        return shape;
    }

    /**
     * Load a prop model without adding it to the scene.
     *
     * @param propType which kind of prop to load (not null)
     */
    private Node loadModel(String propType) {
        assert propType != null;
        /*
         * Load the prop model.
         */
        Node node = Assets.loadPropModel(propType);
        /*
         * Generate a unique name for the new prop.
         */
        NameGenerator nameGenerator = scene.getNameGenerator();
        String name = nameGenerator.unique(propType);
        node.setName(name);

        return node;
    }

    /**
     * Determine the correct material for a name prop part.
     *
     * @param partName the name of the prop part (not null)
     * @return name of the material (or null if unknown part)
     */
    private static String material(String partName) {
        assert partName != null;
        switch (partName) {
            case "barrel1":
            case "mantlet2":
                return "bronze";
            case "barrel2":
            case "mantlet1":
                return "wood";
        }
        logger.log(Level.SEVERE, "Unexpected prop part {0}",
                MyString.quote(partName));
        return null;
    }

    /**
     * Determine the mass of a standard-sized prop.
     *
     * @param propType the type of the prop (not null)
     * @return mass (in kilograms, &gt;0)
     */
    private static float standardMass(String propType) {
        assert propType != null;
        switch (propType) {
            case "barrel":
                return 500f;
            case "mantlet":
                return 100f;
        }
        logger.log(Level.SEVERE, "Unexpected prop type {0}",
                MyString.quote(propType));
        return 1f;
    }
}